﻿Option Strict On
Option Explicit On

''' <summary>
''' Represents a collection of {TKey} keys and multiple types of {TValue} values, where distinct {TValue} types are stored in unique, internal Dictionary(Of TKey, TValue) instances.
''' </summary>
''' <typeparam name="TKey">The type of the keys for all {TValue} types in the DictionaryBank.</typeparam>
''' <remarks>
''' Then DictionaryBank automatically maintains an internal dictionary which is keyed by Type. When items are added to the DictionaryBank
''' using SetValue(Of TValue)(), the DictionaryBank checks for an internal entry whose key is TValue's type. If one is found, then that
''' instance of Dictionary(Of TKey, TValue) is used to add or set the key/value pair passed to the SetValue(Of TValue)() method. 
''' If an internal Dictionary(Of TKey, TValue) is not found for type TValue, then one is created, added to the DictionaryBank, and then
''' used to store the supplied key/value pair.  The Clear() methods will remove entries from an internal dictionary, but will not remove
''' empty dictionaries themselves from the DictionaryBank. To remove internal dictionaries from the DictionaryBank entirely, use an
''' overload of the Remove() method.  Use the Trim() method to remove all empty internal dictionaries from the DictionaryBank.
'''</remarks>
<Serializable()>
Public Class DictionaryBank(Of TKey)
    Protected _TypeDictionary As New Dictionary(Of Type, IDictionary)

    Protected _CanSerialize As Boolean = True
    ''' <summary>
    ''' Indicates if this instance can be serialized.
    ''' This property returns true providing that the DictionaryBank only contains types which support serialization.
    ''' </summary>
    ''' <value></value>
    ''' <returns>False if the DictionaryBank contains any type which does not support serialization; otherwise, true.</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property CanSerialize As Boolean
        Get
            Return _CanSerialize
        End Get
    End Property

    ''' <summary>
    ''' Creates a new instance of DictionaryBank from the existing file specified.
    ''' </summary>
    ''' <param name="fromFile">The name and path to the file which contains the binary serialization to load.</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function Load(ByVal fromFile As String) As DictionaryBank(Of TKey)
        Dim file As System.IO.FileStream = System.IO.File.Open(fromFile, IO.FileMode.Open)
        Return Load(file)
    End Function

    ''' <summary>
    ''' Creates a new instance of DictionaryBank from the IO stream provided.
    ''' </summary>
    ''' <param name="targetStream">The stream which contains the binary serialization to load.</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function Load(ByVal targetStream As System.IO.Stream) As DictionaryBank(Of TKey)
        Dim result As DictionaryBank(Of TKey)
        Dim bf As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
        result = CType(bf.Deserialize(targetStream), DictionaryBank(Of TKey))
        targetStream.Close()
        targetStream.Dispose()
        Return result
    End Function

    ''' <summary>
    ''' Indicates if the specified type supports serialization.
    ''' </summary>
    ''' <param name="objectType">The type to examine.</param>
    ''' <returns>True if instances of this type support serialization; otherwise, false.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Shared Function IsSerializable(ByVal objectType As Type) As Boolean
        Return objectType.IsSerializable
    End Function

    ''' <summary>
    ''' Indicates if instances of objects of the specified type support serialization.
    ''' </summary>
    ''' <param name="objectInstance">The instance of an object whose type will be examined.</param>
    ''' <returns>True if instances of this type support serialization; otherwise, false.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Shared Function IsSerializable(ByVal objectInstance As Object) As Boolean
        Dim ofType As Type = objectInstance.GetType
        Return ofType.IsSerializable
    End Function

    ''' <summary>
    ''' Clears all values of the specfied type from the DictionaryBank. This does not remove the base type dictionary.
    ''' </summary>
    ''' <typeparam name="TValue">The type of {TValue} whose dictionary should be cleared.</typeparam>
    ''' <remarks></remarks>
    Public Overridable Sub Clear(Of TValue)()
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            _TypeDictionary(ofType).Clear()
        End If
    End Sub

    ''' <summary>
    ''' Clears all values from all type dicitionaries in the DictionaryBank. This does not remove the base type dictionaries.
    ''' </summary>
    ''' <remarks></remarks>
    Public Overridable Sub ClearAll()
        For Each keyType As Type In _TypeDictionary.Keys
            _TypeDictionary(keyType).Clear()
        Next
    End Sub

    ''' <summary>
    ''' Indicates if this DictionaryBank contains a type dictionary for the type {TValue}.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Function ContainsDictionary(Of TValue)() As Boolean
        Dim ofType As Type = GetType(TValue)
        Return _TypeDictionary.ContainsKey(ofType)
    End Function

    ''' <summary>
    ''' Indicates if the DictionaryBank contains a value of {TValue} with the given key.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <param name="key"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function ContainsKey(Of TValue)(ByVal key As TKey) As Boolean
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Return _TypeDictionary(ofType).Contains(key)
        End If
        Return False
    End Function

    ''' <summary>
    ''' Returns the number of items of type {TValue} contained in this DictionaryBank.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Count(Of TValue)() As Integer
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Return _TypeDictionary(ofType).Count
        End If
        Return 0
    End Function

    ''' <summary>
    ''' Returns the total number of items of every type contained in this DictionaryBank.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Count() As Integer
        Dim total As Integer = 0
        For Each keyType As Type In _TypeDictionary.Keys
            total += _TypeDictionary(keyType).Count
        Next
        Return total
    End Function

    ''' <summary>
    ''' Gets all keys of the given type {TValue} contained in this DictionaryBank.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetKeys(Of TValue)() As TKey()
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Dim entry As IDictionary
            entry = _TypeDictionary(ofType)
            Dim result(entry.Count - 1) As TKey
            Dim index As Integer
            For Each key As TKey In entry.Keys
                result(index) = key
                index += 1
            Next
            Return result
        End If
        Return New TKey() {}
    End Function

    ''' <summary>
    ''' Returns an array of Type objects containing an entry for each unique {TValue} type contained in this DictionaryBank.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Function GetTypeList() As Type()
        Dim result As New List(Of Type)
        For Each keyType As Type In _TypeDictionary.Keys
            result.Add(keyType)
        Next
        Return result.ToArray
    End Function

    ''' <summary>
    ''' Gets the value of the given type {TValue} for the key specified.
    ''' If no type dictionary exists for {TValue}, or the key is not found, this method returns Nothing.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <param name="key"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function GetValue(Of TValue)(ByVal key As TKey) As TValue
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Dim entry As IDictionary
            entry = _TypeDictionary(ofType)
            If entry.Contains(key) Then
                Return DirectCast(entry(key), TValue)
            End If
        End If
        Return Nothing
    End Function

    ''' <summary>
    ''' Gets all values of the given type {TValue} contained in this DictionaryBank.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetValues(Of TValue)() As TValue()
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Dim entry As IDictionary
            entry = _TypeDictionary(ofType)
            Dim result(entry.Count - 1) As TValue
            Dim index As Integer
            For Each key As TKey In entry.Keys
                result(index) = DirectCast(entry(key), TValue)
                index += 1
            Next
            Return result
        End If
        Return New TValue() {}
    End Function

    ''' <summary>
    ''' Gets an array of object values for all types contained in this DictionaryBank which have the specified key.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetValues(ByVal key As TKey) As Object()
        Dim result As New List(Of Object)
        For Each keyType As Type In _TypeDictionary.Keys
            If _TypeDictionary.ContainsKey(keyType) Then
                Dim entry As IDictionary
                entry = _TypeDictionary(keyType)
                If entry.Contains(key) Then
                    result.Add(entry(key))
                End If
            End If
        Next
        Return result.ToArray
    End Function

    ''' <summary>
    ''' Gets an array of object values for all types specified by the valueTypes parameter contained in this DictionaryBank which have the specified key.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <param name="valueTypes"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetValues(ByVal key As TKey, ByVal ParamArray valueTypes() As Type) As Object()
        Dim result As New List(Of Object)
        For Each keyType As Type In valueTypes
            If _TypeDictionary.ContainsKey(keyType) Then
                Dim entry As IDictionary
                entry = _TypeDictionary(keyType)
                If entry.Contains(key) Then
                    result.Add(entry(key))
                End If
            End If
        Next
        Return result.ToArray
    End Function

    ''' <summary>
    '''  Removes the value for the specified key from the existing type dicitionary for {TValue}, if one exists.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <param name="key"></param>
    ''' <returns>True if an item with the specified key was found in the type dictionary for {TValue}, and removed; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function Remove(Of TValue)(ByVal key As TKey) As Boolean
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            Dim entry As IDictionary
            entry = _TypeDictionary(ofType)
            If entry.Contains(key) Then
                entry.Remove(key)
                Return True
            End If
        End If
        Return False
    End Function

    ''' <summary>
    ''' Removes all type dictionaries from the DictionaryBank and resets the CanSerialize property to true.
    ''' </summary>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Sub RemoveAll()
        _TypeDictionary.Clear()
        _CanSerialize = True
    End Sub

    ''' <summary>
    ''' Removes the type dictionary for the specified type from the DictionaryBank, updates the serialization status, and returns true if successful.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <returns></returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Function RemoveDictionary(Of TValue)() As Boolean
        Dim ofType As Type = GetType(TValue)
        If _TypeDictionary.ContainsKey(ofType) Then
            _TypeDictionary.Remove(ofType)
            UpdateSerializationState()
            Return True
        End If
        Return False
    End Function

    ''' <summary>
    ''' Saves this instance of DictionaryBank to a binary data file on disk.
    ''' </summary>
    ''' <param name="toFile"></param>
    ''' <remarks></remarks>
    Public Sub Save(ByVal toFile As String)
        If Not _CanSerialize Then Throw New SerializationStateException
        Dim file As System.IO.FileStream = System.IO.File.Open(toFile, IO.FileMode.Create)
        Save(file, False)
        file.Dispose()
    End Sub

    ''' <summary>
    ''' Writes this instance of DictionaryBank to a bindary stream.
    ''' </summary>
    ''' <param name="targetStream"></param>
    ''' <param name="leaveOpen"></param>
    ''' <remarks></remarks>
    Public Sub Save(ByVal targetStream As System.IO.Stream, Optional ByVal leaveOpen As Boolean = False)
        If Not _CanSerialize Then Throw New SerializationStateException
        Dim bf As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
        bf.Serialize(targetStream, Me)
        If Not leaveOpen Then targetStream.Close()
    End Sub

    ''' <summary>
    ''' Sets the value for the specified key. If a type dicitionary for {TValue} does not exist, one will be created.
    ''' </summary>
    ''' <typeparam name="TValue"></typeparam>
    ''' <param name="key"></param>
    ''' <param name="value"></param>
    ''' <remarks></remarks>
    Public Overridable Sub SetValue(Of TValue)(ByVal key As TKey, ByVal value As TValue)
        Dim entry As IDictionary
        entry = GetDictionary(GetType(TValue))
        If entry.Contains(key) Then
            entry(key) = value
        Else
            entry.Add(key, value)
        End If
    End Sub

    ''' <summary>
    ''' Removes empty type dictionaries from the DictionaryBank and returns the number of type dictionaries removed.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Trim() As Integer
        Dim removeList As New List(Of Type)
        For Each keyType As Type In _TypeDictionary.Keys
            Dim entry As IDictionary
            entry = _TypeDictionary(keyType)
            If entry.Count = 0 Then removeList.Add(keyType)
        Next
        Dim result As Integer = removeList.Count
        For Each keyType As Type In removeList
            _TypeDictionary.Remove(keyType)
        Next
        Return result
    End Function

    Protected Function GetDictionary(ByVal ofType As Type) As IDictionary
        If _TypeDictionary.ContainsKey(ofType) Then
            Return _TypeDictionary(ofType)
        Else
            Dim entry As New Dictionary(Of TKey, Object)
            _TypeDictionary.Add(ofType, entry)
            If Not ofType.IsSerializable Then
                _CanSerialize = False
            End If
            Return entry
        End If
    End Function

    Protected Sub UpdateSerializationState()
        _CanSerialize = True
        For Each keyType As Type In _TypeDictionary.Keys
            If Not keyType.IsSerializable Then
                _CanSerialize = False
                Exit For
            End If
        Next
    End Sub

    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Class SerializationStateException
        Inherits InvalidOperationException
        Public Sub New()
            MyBase.New("The DictionaryBank contains one or more types which are not serializable.")
        End Sub
    End Class
End Class
